Skip to content

fix(files): stop a cached collab snapshot resurrecting blank lines - #6293

Merged
waleedlatif1 merged 2 commits into
stagingfrom
fix/files-editor-whitespace-image
Aug 5, 2026
Merged

fix(files): stop a cached collab snapshot resurrecting blank lines#6293
waleedlatif1 merged 2 commits into
stagingfrom
fix/files-editor-whitespace-image

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • A collaborative markdown file's cold-start seed can come from a cached Yjs snapshot (workspace_file_collab_state.doc_state) instead of a fresh markdown re-parse. That snapshot is a raw CRDT binary, so it preserves top-level empty paragraphs that parseMarkdownToDocstripEmptyParagraphs strips from every parse target.
  • The static placeholder always re-parses (clean); a warm seed replays the snapshot verbatim, so a stray blank line appears once the doc settles — and only intermittently, because a stale/cold cache falls through to the clean re-parse.
  • Proven from production data: decoded the persisted snapshot for the reported staging file and found two consecutive len=0 paragraphs sitting exactly between the description paragraph and the bullet list (plus one more before the trailing link) — the reported "two spaces."
  • This is the warm-seed repair explicitly flagged as owed when the parse-side collapse fix (fix(files): collapse blank-line runs to markdown standard (no empty-paragraph explosion / reflow) #6198) shipped — now self-healing at seed time, so no data migration is needed.

Changes

  • lib/collab-doc/normalize.tsstripEmptyTopLevelParagraphs(doc) shared pure-Yjs helper enforcing the same no-top-level-empty-paragraph invariant on the CRDT side.
  • lib/collab-doc/seed.ts — repair the cached snapshot on read (self-heals legacy snapshots, preserving CRDT client ids; returns original bytes untouched when already clean).
  • lib/collab-doc/persist.ts — normalize before caching so new snapshots are clean by construction.

Type of Change

  • Bug fix

Testing

  • normalize.test.ts: 5 tests (production repro, idempotency, no-op-when-clean, nested-untouched, encode/decode preserves config + CRDT ids). Verified they go red when the helper is neutralized.
  • Full lib/collab-doc suite (24 tests) green; type-check clean.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

A collaborative markdown file's cold-start seed can come from a cached Yjs
snapshot (workspace_file_collab_state.doc_state) rather than a fresh markdown
re-parse. The snapshot is a raw CRDT binary, so it preserves top-level empty
paragraphs that parseMarkdownToDoc/stripEmptyParagraphs strips from every parse
target. The static placeholder always re-parses (clean); a warm seed replays the
snapshot verbatim, so a stray blank line appears once the doc settles — and only
intermittently, since a stale/cold cache falls through to the clean re-parse.

Enforce the same no-top-level-empty-paragraph invariant on the Yjs side:
- normalize.ts: stripEmptyTopLevelParagraphs(doc) shared helper.
- seed.ts: repair the cached snapshot on read (self-heals legacy snapshots,
  preserving CRDT client ids; no data migration).
- persist.ts: normalize before caching so new snapshots are clean by construction.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 5, 2026 8:26pm

Request Review

@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Collab doc normalization only; no auth or durable markdown semantics change beyond aligning cached CRDT state with existing parse rules.

Overview
Fixes intermittent stray blank lines when a collaborative file cold-starts from a cached Yjs snapshot instead of a fresh markdown re-parse. Raw CRDT binaries can keep top-level empty paragraph nodes that parseMarkdownToDoc / stripEmptyParagraphs already drop, so the warm seed diverges from the static placeholder once the doc settles.

Adds stripEmptyTopLevelParagraphs in normalize.ts (shared with COLLAB_DOC_FIELD moved there from converter.ts) to enforce the same invariant on the Yjs body fragment. Seed repairs legacy cached snapshots on read via normalizeSeedUpdate (re-encode only when needed). Persist strips empties before writing the collab cache so new snapshots stay clean. normalize.test.ts covers the production repro, idempotency, nested paragraphs, and encode/decode round-trips.

Reviewed by Cursor Bugbot for commit 72c1a4a. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR normalizes cached collaborative Yjs documents when they are seeded and persisted, preventing legacy empty top-level paragraphs from resurfacing.

  • Adds a shared top-level paragraph normalization helper and focused tests.
  • Repairs cached snapshots during room seeding and stores normalized snapshots during persistence.
  • Centralizes the collaborative document field name for conversion and normalization.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/collab-doc/normalize.ts Adds the shared Yjs fragment constant and an idempotent helper that removes empty top-level paragraphs.
apps/sim/lib/collab-doc/seed.ts Repairs legacy cached snapshots before using them to seed collaborative rooms.
apps/sim/lib/collab-doc/persist.ts Normalizes the Yjs document before deriving markdown and caching its snapshot.
apps/sim/lib/collab-doc/normalize.test.ts Covers production-shaped normalization, idempotency, nested content, clean no-ops, and snapshot metadata preservation.
apps/sim/lib/collab-doc/converter.ts Reuses the canonical collaborative document field constant.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Cache[Cached Yjs snapshot] --> Seed[Seed-time normalization]
  Live[Live Yjs document] --> Persist[Persist-time normalization]
  Seed --> Room[Collaborative room]
  Persist --> Markdown[Durable markdown]
  Persist --> CleanCache[Normalized cached snapshot]
Loading

Reviews (2): Last reviewed commit: "refactor(collab-doc): make COLLAB_DOC_FI..." | Re-trigger Greptile

Comment thread apps/sim/lib/collab-doc/normalize.ts
converter.ts had a duplicate 'default' fragment-name constant; import the now-exported one from normalize.ts so the value TipTap's Collaboration binding depends on lives in exactly one place. Fold the back-to-front loop note into the helper's TSDoc.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 72c1a4a. Configure here.

@waleedlatif1
waleedlatif1 merged commit 0bef1c3 into staging Aug 5, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/files-editor-whitespace-image branch August 5, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant